/*
Copyright (C) 2008, 2009 Henrik Naess

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/

using System;
using System.Collections.Generic;
using System.Data;
using System.Windows.Forms;
using MatrixPlugin.Constants;
using MatrixPlugin.Integration;
using MatrixPlugin.Properties;
using MatrixPlugin.Utils;
using ZoneFiveSoftware.Common.Data;
using ZoneFiveSoftware.Common.Data.Fitness;
using ZoneFiveSoftware.Common.Data.Measurement;
using ZoneFiveSoftware.Common.Data.GPS;

namespace MatrixPlugin
{
    public class ViewerController
    {
        #region private variables

        private readonly IDictionary<string, DistanceInfoList> _cachedDistanceLists =
            new Dictionary<string, DistanceInfoList>();

        private PositionGroup _positionGroup = null;
        private readonly SettingsController _settingsController = SettingsController.Instance();

        /// <summary>header text for row in the datatable</summary
        private readonly IList<ExtendedGPSPoint> _rowHeaderText = new List<ExtendedGPSPoint>();

        /// <summary>header text for columns in the datatable</summary
        private readonly IList<ExtendedGPSPoint> _columnHeaderText = new List<ExtendedGPSPoint>();

        private IList<IActivity> m_activities = new List<IActivity>();

        /// <summary>Used to remember base activity, when changing from one to several (unique routes)</summary>
        private IActivity m_baseActivity;

        private DataSource m_dataSource = DataSource.CurrentSelection;

        #endregion

        #region Constructor

        internal ViewerController()
        {
        }

        #endregion

        #region Properties

        public IList<IActivity> Activities
        {
            get { return m_activities; }
            set
            {
                if (value != m_activities)
                {
                    bool update = true;
                    if ((value != null && m_activities != null) && (value.Count == m_activities.Count))
                    {
                        update = false;
                        foreach (IActivity activity in value)
                        {
                            if (!m_activities.Contains(activity))
                            {
                                update = true;
                                break;
                            }
                        }
                    }

                    if (update)
                    {
                        m_activities = value;

                        if (PositionGroup != null)
                        {
                            PositionGroup.ClearCalculations();
                        }
                        _cachedDistanceLists.Clear();
                    }
                    if (value == null || value.Count == 0)
                    {
                        m_baseActivity = null;
                    }
                    else
                    {
                        bool isMatch = false;
                        if (m_baseActivity != null)
                        {
                            foreach (IActivity t in value)
                            {
                                if (t.Equals(m_baseActivity))
                                {
                                    isMatch = true;
                                    break;
                                }
                            }
                        }
                        if (!isMatch)
                        {
                            m_baseActivity = value[0];
                        }
                    }
                }
            }
        }
        public void Reset()
        {
            _settingsController.Reset();
        }

        public bool HasBaseActivity
        {
            get { return m_baseActivity != null && this.Activities.Count > 1; }
        }
        public IActivity BaseActivity
        {
            get { return m_baseActivity; }
            set {
                m_baseActivity = value;
            }
        }

        /// <summary>List of GPS Points in the row header</summary>
        public IList<ExtendedGPSPoint> ColumnHeaderText
        {
            get { return _columnHeaderText; }
        }

        /// <summary>List of GPS Points in the column header</summary>
        public IList<ExtendedGPSPoint> RowHeaderText
        {
            get { return _rowHeaderText; }
        }

        public int NumberOfActivities
        {
            get { return m_activities != null ? m_activities.Count : 0; }
        }

        public int NumberOfActivitiesWithGPSdata
        {
            get
            {
                int count = 0;
                if (m_activities != null)
                {
                    foreach (IActivity activity in Activities)
                    {
                        if (activity != null && activity.GPSRoute != null)
                            count++;
                    }
                }
                return count;
            }
        }
        public DataSource dataSource
        {
            get
            {
                return m_dataSource;
            }
            set
            {
                m_dataSource = value;
            }
        }

        public string GetNumberOfActivitiesText()
        {
            int numberOfActivitiesWithGPSdata = NumberOfActivitiesWithGPSdata;
            int numberOfActivities = NumberOfActivities;

            string text;
            if (numberOfActivities == 1)
                text = "1 " + ZoneFiveSoftware.Common.Visuals.CommonResources.Text.LabelActivity;
            else
                text = string.Format("{0} {1}", numberOfActivities, Resources.ActivitiesText);

            if (numberOfActivitiesWithGPSdata != numberOfActivities)
                text += string.Format(" ({0} {1})", numberOfActivitiesWithGPSdata, Resources.TableViewer_WithGPSText);

            return text;
        }

        /// <summary>
        /// Set position group that should be for calculation
        /// </summary>
        /// <exception cref="KeyNotFoundException">if key doesn't exists (gui not sync with settings)</exception>
        public PositionGroup PositionGroup
        {
            set
            {
                if (value != _positionGroup)
                {
                    _cachedDistanceLists.Clear();
                    _positionGroup = value;// _settingsController.PositionGroups[value];
                    if (_positionGroup != null)
                    {
                        _positionGroup.Controller = this;
                        _positionGroup.ClearCalculations();
                    }
                }
            }
            get
            {
                if (_positionGroup == null && !string.IsNullOrEmpty(Settings.PositionGroup))
                {
                    if (_settingsController.PositionGroups.ContainsKey(Settings.PositionGroup))
                    {
                        _positionGroup = _settingsController.PositionGroups[Settings.PositionGroup];
                        _positionGroup.Controller = this;
                    }

                } 
                return _positionGroup;
            }
        }

        #endregion

        /// <summary>
        /// Calculate data for activities
        /// </summary>
        /// <param name="loadingProgressbar">progressbar for display loading</param>
        public DataTable Calculate(System.Windows.Forms.ProgressBar loadingProgressbar)
        {
            DateTime startTimeCalculation = DateTime.Now;
            DataTable table = new DataTable();
            ColumnHeaderText.Clear();
            RowHeaderText.Clear();

            if (PositionGroup != null && _positionGroup.MatrixPoints != null)
            {
                AddColumnHeader(table);

                if (Activities != null)
                {
                    bool validCache = _cachedDistanceLists.Count > 0;

                    loadingProgressbar.Value = 0;
                    loadingProgressbar.Minimum = 0;
                    loadingProgressbar.Maximum = _positionGroup.MatrixPoints.Count;
                    loadingProgressbar.Step = 1;

                    if (!validCache)
                    {
                        loadingProgressbar.Maximum += Activities.Count;
                        ProcessActivities(Activities, startTimeCalculation, loadingProgressbar);
                    }

                    for (int point1Iterator = 0; point1Iterator < _positionGroup.MatrixPoints.Count; point1Iterator++)
                    {
                        PerformStepOnProgressBar(startTimeCalculation, loadingProgressbar);
                        MatrixPoint point1 = _positionGroup.MatrixPoints[point1Iterator];

                        DataRow row = table.NewRow();
                        int column = 0;
                        for (int point2Iterator = 0; point2Iterator < _positionGroup.MatrixPoints.Count; point2Iterator++)
                        {
                            string cell = null;
                            MatrixPoint point2 = _positionGroup.MatrixPoints[point2Iterator];

                                string cacheKey = column + "&" + table.Rows.Count;
                                DistanceInfoList distanceInfoList = null;
                                if (!validCache)
                                    distanceInfoList = point1.GetDistanceData(point2, Settings.IncludeStoppedTime);
                                else
                                    _cachedDistanceLists.TryGetValue(cacheKey, out distanceInfoList);

                                if (distanceInfoList != null && distanceInfoList.Count > 0)
                                {
                                    distanceInfoList.SortOrder = DistanceInfoComparer.GetSortOrder(Settings.DataValue);
                                    DistanceInfo distanceInfo = DistanceInfoList.GetDistanceInfoForTempoType(distanceInfoList, Settings.TempoType);
                                    cell = GetCellText(distanceInfo, distanceInfoList);

                                    if (!validCache)
                                        _cachedDistanceLists.Add(cacheKey, distanceInfoList);
                                }
                            row[column++] = cell;
                        }
                        RowHeaderText.Add(point1);

                        table.Rows.Add(row);
                    }
                }
            }
            return table;
        }

        private void AddColumnHeader(DataTable table)
        {
            foreach (MatrixPoint point in PositionGroup.MatrixPoints)
            {
                table.Columns.Add(point.Name);
                ColumnHeaderText.Add(point);
            }
        }

        private string GetCellText(DistanceInfo distanceInfo, DistanceInfoList distanceInfoList)
        {
            DistanceInfo baseDistanceInfo = null;
            if (Settings.CompareToCurrent && HasBaseActivity)
                baseDistanceInfo = distanceInfoList.FindActivity(m_baseActivity.ReferenceId);

            string cell;
            if (baseDistanceInfo != null)
            {
                DistanceInfo foundActivity = baseDistanceInfo;
                switch (Settings.DataValue)
                {
                    case General.DataValue.speed:
                        cell = distanceInfo.SpeedStringComparedWithBaseActivity(foundActivity);
                        break;
                    case General.DataValue.pace:
                        cell = distanceInfo.PaceStringComparedWithBaseActivity(foundActivity);
                        break;
                    case General.DataValue.elevation:
                        cell = distanceInfo.ElevationStringComparedWithBaseActivity(foundActivity);
                        break;
                    case General.DataValue.distance:
                        cell = distanceInfo.DistanceStringComparedWithBaseActivity(foundActivity);
                        break;
                    default:
                        cell = distanceInfo.EffectiveTimeComparedWithBaseActivity(foundActivity);
                        break;
                }
            }
            else
            {
                switch (Settings.DataValue)
                {
                    case General.DataValue.speed:
                        cell = distanceInfo.SpeedString;
                        break;
                    case General.DataValue.pace:
                        cell = distanceInfo.PaceString;
                        break;
                    case General.DataValue.elevation:
                        cell = distanceInfo.ElevationString;
                        break;
                    case General.DataValue.distance:
                        cell = distanceInfo.DistanceString;
                        break;
                    default:
                        cell = distanceInfo.EffectiveTimeString;
                        break;
                }
            }

            return cell;
        }

        private static float PointSpeed(ITimeValueEntry<IGPSPoint> entry1, ITimeValueEntry<IGPSPoint> entry2)
        {
            float seconds = entry2.ElapsedSeconds - entry1.ElapsedSeconds;
            float speed = 0;
            if (seconds > 0)
            {
                float distance = entry2.Value.DistanceMetersToPoint(entry1.Value);
                speed = (distance / seconds);
            }
            return speed;
        }
        private void ProcessActivities(IEnumerable<IActivity> activities, DateTime startTimeCalcuation, ProgressBar loadingProgressbar)
        {
            foreach (IActivity activity in activities)
            {
                PerformStepOnProgressBar(startTimeCalcuation, loadingProgressbar);
                if (activity != null && activity.GPSRoute != null)
                {
                    DateTime startTime = activity.GPSRoute.StartTime;
                    TimeSpan stoppedTime = new TimeSpan();

                    GPSRoute tempRoute = new GPSRoute();
                    tempRoute.AllowMultipleAtSameTime = true;

                    ITimeValueEntry<IGPSPoint> prevPoint = null;
                    foreach (ITimeValueEntry<IGPSPoint> currPoint in activity.GPSRoute)
                    {
                        tempRoute.Add(startTime.AddSeconds(currPoint.ElapsedSeconds), currPoint.Value);

                        // FindSubActivity stopped time
                        //TODO: Should use ST calc (so smoothing is considered), by creating activity and using ActivityInfo
                        //also get summary for distance
                        if (null != prevPoint)
                        {
                            if (PointSpeed(prevPoint, currPoint) < StoppedSpeedForCategory(activity.Category))
                            {
                                //Type casting needed for ST_3_0, not ST_2_1
                                int seconds = (int)currPoint.ElapsedSeconds - (int)prevPoint.ElapsedSeconds;
                                stoppedTime += new TimeSpan(0, 0, 0, seconds);
                            }
                        }

                        foreach (MatrixPoint point in PositionGroup.MatrixPoints)
                        {
                            point.CheckForNewClosestEntry(activity, currPoint, tempRoute, stoppedTime, prevPoint);
                        }
                        prevPoint = currPoint;
                    }
                }
            }
        }

        /// <summary>
        /// Show progressbar if calculations take longer time than X milliseconds
        /// </summary>
        /// <param name="startTimeCalculation">start time for calculations</param>
        /// <param name="progressbar">progressbar displayed when loading/calculations</param>
        private static void PerformStepOnProgressBar(DateTime startTimeCalculation, ProgressBar progressbar)
        {
            progressbar.PerformStep();
            if (progressbar.Visible == false && DateTime.Now.Subtract(startTimeCalculation).TotalMilliseconds > 300)
            {
                progressbar.Visible = true;
            }
        }

        private static double StoppedSpeedForCategory(IActivityCategory findActivityCategory)
        {
            IActivityCategory activityCategory = SportTracks.GetActivityCategory(findActivityCategory);
            double stoppedSpeed = Preferences.LessThanThisSpeedEqualsStopped;

            if (activityCategory != null)
            {
                stoppedSpeed = activityCategory.StoppedMetersPerSecond;
            }
            else
            {
                MessageBox.Show(string.Format(Resources.TableViewer_LinkedToSeveralText, findActivityCategory.Name, stoppedSpeed));
            }
            return stoppedSpeed;
        }

        public bool SetActivityToActivityYear(int year)
        {
            List<IActivity> activitiesThisYear = new List<IActivity>();
            foreach (IActivity activity in Plugin.GetApplication().Logbook.Activities)
            {
                if (activity.StartTime.Year == year)
                    activitiesThisYear.Add(activity);
            }
            Activities = activitiesThisYear;
            return true;
        }

        public bool SetActivityToBaseActivityDate()
        {
            if (m_baseActivity != null)
            {
                List<IActivity> activitiesThisDate = new List<IActivity>();
                foreach (IActivity activity in Plugin.GetApplication().Logbook.Activities)
                {
                    if (activity.StartTime.Date == m_baseActivity.StartTime.Date)
                        activitiesThisDate.Add(activity);
                }
                Activities = activitiesThisDate;
                return true;
            }
            else
                return false;
        }

        public bool SetActivityToAllActivities()
        {
            Activities = (IList<IActivity>)Plugin.GetApplication().Logbook.Activities;
            return true;
        }

        public bool SetActivityToBaseActivity()
        {
            if (m_baseActivity != null)
            {
                Activities = new IActivity[] { m_baseActivity };
                return true;
            }
            else
                return false;
        }

        public bool SetActivityToSelectedActivity(IList<IActivity> activities)
        {
                Activities = activities;
                return true;
        }

        public bool SetActivityBasedOnUniqueRoute()
        {
            bool updated = false;
            if (m_baseActivity != null)
            {
                IActivity activity = m_baseActivity;

                try
                {
                    IList<IActivity> similarActivities = TrailsPlugin.Integration.UniqueRoutes.GetUniqueRoutesForActivity(activity, null);

                    if (similarActivities != null && similarActivities.Count > 0)
                    {
                        Activities = similarActivities;
                        updated = true;
                    }
                }
                catch (OtherPlugInException ex)
                {
                    MessageBox.Show(ex.Message, "Plugin error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
            return updated;
        }
        public bool SetActivityToCategory()
        {
            bool result = false;
            IList<IActivity> categoryActivities = new List<IActivity>();
            foreach (IActivity activity in Activities)
            {
                if (isAllowedActivity(activity))
                {
                    categoryActivities.Add(activity);
                }
                else
                {
                    result = true;
                }
            }
            if (result)
            {
                //Activities changed, update source
                Activities = categoryActivities;
            }
            return result;
        }

        private static bool isAllowedActivity(IActivity activity)
        {
            return activity != null && (Settings.SelectedCategory == null || 
                isSubcategoryOf(Settings.SelectedCategory, activity.Category));
        }

        private static bool isSubcategoryOf(IActivityCategory iActivityCategory, IActivityCategory iActivityCategory_2)
        {
            if (iActivityCategory.Equals(iActivityCategory_2)) return true;
            if (iActivityCategory_2 == null) return false;
            return isSubcategoryOf(iActivityCategory, iActivityCategory_2.Parent);
        }

        public String ToolTip(int column, int row)
        {
            return ToolTipAsDelimitedField(column, row, "");
        }

        public String ToolTipAsDelimitedField(int column, int row, string delimiter)
        {
            DistanceInfoList cachedDistanceInfoList;
            String key = column + "&" + row;

            _cachedDistanceLists.TryGetValue(key, out cachedDistanceInfoList);
            if (cachedDistanceInfoList != null)
            {
                string id = (m_baseActivity == null ? null : m_baseActivity.ReferenceId);
                return cachedDistanceInfoList.AllAsToolTipDelimitedFormated(id, Settings.ToolTipFormatString, delimiter);
            }
            else
                return String.Empty;
        }
        public IDictionary<string, string> ToolTipActivity(int column, int row, string delimiter)
        {
            DistanceInfoList cachedDistanceInfoList;
            String key = column + "&" + row;
            IDictionary<string, string> result = new Dictionary<string, string>();

            _cachedDistanceLists.TryGetValue(key, out cachedDistanceInfoList);
            if (cachedDistanceInfoList != null)
            {
                string id = (m_baseActivity == null ? null : m_baseActivity.ReferenceId);
                result = cachedDistanceInfoList.AllActivitiesAsToolTipDictonary(id, Settings.ToolTipFormatString, delimiter);
            }
            return result;
        }

        #region Item list from comboboxes that may be dynamic

        public List<PositionGroup> GetListForPositionGroups()
        {
            List<PositionGroup> collection = new List<PositionGroup>();
            foreach (KeyValuePair<string, PositionGroup> positionGroup in _settingsController.PositionGroups)
            {
                if (!String.IsNullOrEmpty(positionGroup.Key))
                {
                    collection.Add(positionGroup.Value);
                }
            }
            return collection;
        }

        #endregion
    }
}